Skip to content

Exercises

Alright, let's get some practice with Immer!

Gradient Generator

Below, you'll find our previous solution for the Gradient Generator, using useReducer. Your job is to update it to use Immer.

Acceptance Criteria:

  • You should use the produce function from Immer to produce the new state, within the reducer function
  • Tweak the state-updating logic to edit the draft state using mutation, instead of returning a new state object.
  • The import has already been provided for you, just under the React import.

Code Playground

import React from 'react';
import { produce } from 'immer';

const INITIAL_STATE = {
colors: [
'#FFD500',
'#FF0040',
'#FF0040',
'#FF0040',
'#FF0040',
],
numOfVisibleColors: 2,
};

function reducer(state, action) {
switch (action.type) {
case 'add-color': {
return {
...state,
numOfVisibleColors:
state.numOfVisibleColors + 1,
};
}

case 'remove-color': {
return {
...state,
numOfVisibleColors:
state.numOfVisibleColors - 1,
};
}

case 'change-color': {
const nextColors = [...state.colors];
nextColors[action.index] = action.value;

return {
...state,
colors: nextColors,
};
}
}
}

function App() {
const [state, dispatch] = React.useReducer(
reducer,
INITIAL_STATE
);
const { colors, numOfVisibleColors } = state;

const visibleColors = colors.slice(
0,
numOfVisibleColors
);

const colorStops = visibleColors.join(', ');
const backgroundImage = `linear-gradient(${colorStops})`;

function addColor() {
if (numOfVisibleColors >= 5) {
window.alert(
'There is a maximum of 5 colors'
);
return;
}

dispatch({ type: 'add-color' });
}

function removeColor() {
if (numOfVisibleColors <= 2) {
window.alert(

Solution:

Todo List

Let's update our “Todo List” application to use Immer.

Acceptance Criteria:

  • The reducer should be updated so that Immer is used to update the state
  • Go through each action, and see if we can simplify the state-updating logic using mutation. It's up to you to decide what will work best in each situation!
  • Feel free to edit things beyond the reducer, eg. to change which data gets passed through in the action.

Code Playground

import React from 'react';
import { produce } from 'immer';

import CreateNewTodo from './CreateNewTodo';
import TodoList from './TodoList';

function reducer(todos, action) {
switch (action.type) {
case 'create-todo': {
return [
...todos,
{
value: action.value,
id: crypto.randomUUID(),
},
];
}
case 'toggle-todo': {
return todos.map((todo) => {
if (todo.id !== action.id) {
return todo;
}
return {
...todo,
isCompleted: !todo.isCompleted,
};
});
}
case 'delete-todo': {
return todos.filter((todo) => todo.id !== action.id);
}
}
}

function App() {
const [todos, dispatch] = React.useReducer(reducer, []);

function handleCreateTodo(value) {
dispatch({
type: 'create-todo',
value,
});
}

function handleToggleTodo(id) {
dispatch({
type: 'toggle-todo',
id,
});
}

function handleDeleteTodo(id) {
dispatch({
type: 'delete-todo',
id,
});
}

return (
<div className="wrapper">
<div className="list-wrapper">
<TodoList
todos={todos}
handleToggleTodo={handleToggleTodo}
handleDeleteTodo={handleDeleteTodo}

Solution:

Correction: As I mentioned in the “useReducer” lesson, reducers should be pure functions. As a result, we shouldn't be generating the unique ID within the reducer. The solution below has been updated, so that the ID is passed in through the action:

Pizza Toppings

Below, you'll find a pizza ordering form. All of the UI is done, but there isn't any state management yet. Your mission is to manage the state using useReducer and Immer.

Acceptance Criteria:

  • When the user submits the form, a window.alert should show us what size and toppings they've selected.
  • The radio buttons and checkboxes should be controlled by the reducer's state.
  • The “Select All” button should add all of the toppings.
    • If all of the toppings are selected, however, the button label should flip to "Remove All", and it should toggle all of the toppings off.

This is a challenging exercise. You'll need to figure out how to bind the values of checkboxes/radio buttons to reducer state, which is not something we've explicitly covered! The “Input Cheatsheet” should get you 75% of the way there, but you'll need to do some experimenting to figure it out.

Code Playground

import React from 'react';
import { produce } from 'immer';

function OrderPizza() {
const id = React.useId();
function handleSubmit(event) {
event.preventDefault();
// TODO: call window.alert() with the selected toppings.
}
return (
<form onSubmit={handleSubmit}>
<h2>Your order</h2>
<fieldset>
<legend>Select size:</legend>
<div className="size">
{SIZES.map(({ slug, label }) => {
const inputId = `size-${slug}`;

return (
<label key={inputId} htmlFor={inputId}>
<input
id={inputId}
type="radio"
name={`${id}-size-group`}
/>
{label}
</label>
);
})}
</div>
</fieldset>
<fieldset>
<legend>Select your pizza toppings:</legend>
<div className="toppings">
{TOPPINGS.map(({ slug, label }) => {
const inputId = `topping-${id}-${slug}`;
return (
<label key={inputId} htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
/>
{label}
</label>
);
})}
</div>
<div className="topping-actions">
<button>Select All</button>
</div>
</fieldset>
<div className="actions">
<button>Order pizza</button>
</div>
</form>
);
}

const SIZES = [
{ slug: 'sm', label: 'Small (10")' },
{ slug: 'md', label: 'Medium (12")' },
{ slug: 'lg', label: 'Large (14")' },
{ slug: 'xl', label: 'Pizza For Days (16")' },
]
const TOPPINGS = [
{ slug: 'anchovies', label: 'Anchovies' },
{ slug: 'mushrooms', label: 'Mushrooms' },

Solution:

Correction: There is a more semantic way to prevent the “Select All” button from submitting the form: we can add type="button". That way, we don't need the event.preventDefault(). This improvement has been added to the solution code: